﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Web;
using System.IO.Compression;

namespace Celestus.Iis.Filters
{
    /// <summary>
    /// Provides a generic view and line formatting of a sequence of bytes, supports raw, GZip an Deflate streams.
    /// </summary>
    public class TextWrapperFilter : Stream
    {
        const byte LINE_BREAK = (byte)'\n';

        private int currentLineLength = 0;
        private int maxLineLength;
        private Stream stream;
        private Stream baseStream;
        private byte[] line;

        /// <summary>
        /// Initializes a new instance of the TextWrapperFilter class. 
        /// </summary>
        /// <param name="stream">The base stream.</param>
        /// <param name="lineLength">The max number of characters per line.</param>
        public TextWrapperFilter(Stream stream, int maxLineLength)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }
            if (maxLineLength <= 0)
            {
                throw new ArgumentOutOfRangeException("lineLength", "Value must be bigger than zero.");
            }
            this.stream = stream;
            this.baseStream = stream;
            this.maxLineLength = maxLineLength;
            line = new byte[maxLineLength];
        }

        #region Private Members

        /// <summary>
        /// Processes a byte array.
        /// </summary>
        /// <param name="buffer">An array of bytes. This method process bytes from buffer.</param>
        /// <param name="offset">The zero-based byte offset in buffer at which to processing bytes.</param>
        /// <param name="count">The number of bytes to be processed.</param>
        private void ProcessBuffer(byte[] buffer, int offset, int count)
        {
            for (int i = 0; i < count; i++)
            {
                currentLineLength++;

                line[currentLineLength - 1] = buffer[offset + i];

                if (currentLineLength == 1 && line[0] == ' ')
                {
                    currentLineLength = 0;
                }
                else if (buffer[offset + i] == LINE_BREAK)
                {
                    stream.Write(line, 0, currentLineLength);

                    currentLineLength = 0;
                }
                else if (currentLineLength == maxLineLength)
                {
                    if (offset + i + 1 >= count || buffer[offset + i + 1] == ' ')
                    {
                        stream.Write(line, 0, currentLineLength);
                        stream.WriteByte(LINE_BREAK);

                        currentLineLength = 0;
                    }
                    else
                    {
                        List<byte> list = new List<byte>(line);
                        int lastSpaceIndex = list.FindLastIndex(w => w == ' ');

                        if (lastSpaceIndex >= 0)
                        {
                            stream.Write(line, 0, lastSpaceIndex);
                            stream.WriteByte(LINE_BREAK);

                            currentLineLength = maxLineLength - lastSpaceIndex - 1;

                            for (int j = 0, k = lastSpaceIndex + 1; k < maxLineLength; j++, k++)
                            {
                                line[j] = line[k];
                            }
                        }
                        else
                        {
                            stream.Write(line, 0, currentLineLength);
                            stream.WriteByte(LINE_BREAK);

                            currentLineLength = 0;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Processes a byte array encoded with GZip and initiates GZip processing mode.
        /// </summary>
        /// <param name="buffer">An GZip encoded array of bytes. This method decodes bytes from buffer to the new byte array.</param>
        /// <param name="offset">The zero-based byte offset in buffer at which to begin decoding bytes to the new byte array.</param>
        /// <param name="count">The number of bytes to be decoded to the new byte array.</param>
        private void ProcessGZipStream(ref byte[] buffer, ref int offset, ref int count)
        {
            using (MemoryStream memoryStream = new MemoryStream(buffer))
            {
                using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
                {
                    int readBytes;
                    List<byte> listBuffer = new List<byte>();
                    byte[] readBuffer = new byte[512];

                    while ((readBytes = gzipStream.Read(readBuffer, 0, 512)) != 0)
                    {
                        if (readBytes != 512)
                        {
                            for (int i = 0; i < readBytes; i++)
                            {
                                listBuffer.Add(readBuffer[i]);
                            }
                        }
                        else
                        {
                            listBuffer.AddRange(readBuffer);
                        }
                    }
                    buffer = listBuffer.ToArray();
                    offset = 0;
                    count = listBuffer.Count;
                }
            }
            stream = new GZipStream(baseStream, CompressionMode.Compress);
        }

        /// <summary>
        /// Processes a byte array encoded with Deflate and initiates Deflate processing mode.
        /// </summary>
        /// <param name="buffer">An Deflate encoded array of bytes. This method decodes bytes from buffer to the new byte array.</param>
        /// <param name="offset">The zero-based byte offset in buffer at which to begin decoding bytes to the new byte array.</param>
        /// <param name="count">The number of bytes to be decoded to the new byte array.</param>
        private void ProcessDeflateStream(ref byte[] buffer, ref int offset, ref int count)
        {
            using (MemoryStream memoryStream = new MemoryStream(buffer))
            {
                using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionMode.Decompress))
                {
                    int readBytes;
                    List<byte> listBuffer = new List<byte>();
                    byte[] readBuffer = new byte[512];

                    while ((readBytes = deflateStream.Read(readBuffer, 0, 512)) != 0)
                    {
                        if (readBytes != 512)
                        {
                            for (int i = 0; i < readBytes; i++)
                            {
                                listBuffer.Add(readBuffer[i]);
                            }
                        }
                        else
                        {
                            listBuffer.AddRange(readBuffer);
                        }
                    }
                    buffer = listBuffer.ToArray();
                    offset = 0;
                    count = listBuffer.Count;
                }
            }
            stream = new DeflateStream(baseStream, CompressionMode.Compress);
        }
        
        /// <summary>
        /// Creates a byte array of the subset given by the <paramref name="buffer"/> and the <paramref name="count"/>.
        /// </summary>
        /// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the new byte array.</param>
        /// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the new byte array.</param>
        /// <param name="count">The number of bytes to be copied to the new byte array.</param>
        private void TrimByteArray(ref byte[] buffer, ref int offset, int count)
        {
            byte[] tmpBuffer = new byte[count];

            for (int i = 0; i < count; i++)
            {
                tmpBuffer[i] = buffer[offset + i];
            }
            buffer = tmpBuffer;
            offset = 0;
        }

        #endregion

        #region Stream Members

        /// <summary>
        /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
        /// </summary>
        /// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
        /// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
        /// <param name="count">The number of bytes to be written to the current stream.</param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            HttpResponse response = HttpContext.Current.Response;
            string encoding = response.Headers["Content-Encoding"];

            if (encoding == "gzip")
            {
                if (count != buffer.Length)
                {
                    TrimByteArray(ref buffer, ref offset, count);
                }
                ProcessGZipStream(ref buffer, ref offset, ref count);
            }
            else if (encoding == "deflate")
            {
                if (count != buffer.Length)
                {
                    TrimByteArray(ref buffer, ref offset, count);
                }
                ProcessDeflateStream(ref buffer, ref offset, ref count);
            }

            if (!string.IsNullOrEmpty(encoding) && encoding != "gzip" && encoding != "deflate")
            {
                stream.Write(buffer, offset, count);
            }
            else
            {
                ProcessBuffer(buffer, offset, count);
            }
        }

        /// <summary>
        /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
        /// </summary>
        public override void Flush()
        {
            if (currentLineLength != 0)
            {
                stream.Write(line, 0, currentLineLength);
            }
            stream.Flush();
            baseStream.Flush();
        }

        /// <summary>
        /// Closes the current stream and releases any resources associated with the current stream.
        /// </summary>
        public override void Close()
        {
            if (stream == baseStream)
            {
                stream.Close();
            }
            else
            {
                stream.Close();
                baseStream.Close();
            }
        }

        /// <summary>
        /// Releases all resources used by this Stream.
        /// </summary>
        /// <param name="disposing">This parameter is ignored.</param>
        protected override void Dispose(bool disposing)
        {
            if (stream == baseStream)
            {
                stream.Dispose();
            }
            else
            {
                stream.Dispose();
                baseStream.Dispose();
            }
        }

        /// <summary>
        /// Not implemented.
        /// </summary>
        public override bool CanRead
        {
            get 
            {
                throw new NotImplementedException(); 
            }
        }

        /// <summary>
        /// Not implemented.
        /// </summary>
        public override bool CanSeek
        {
            get
            {
                throw new NotImplementedException(); 
            }
        }

        /// <summary>
        /// Not implemented.
        /// </summary>
        public override bool CanWrite
        {
            get
            {
                throw new NotImplementedException(); 
            }
        }

        /// <summary>
        /// Not implemented.
        /// </summary>
        public override long Length
        {
            get
            { 
                throw new NotImplementedException(); 
            }
        }

        /// <summary>
        /// Not implemented.
        /// </summary>
        public override long Position
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// Not implemented.
        /// </summary>
        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Not implemented.
        /// </summary>
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Not implemented.
        /// </summary>
        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}
